import { treeDiff, type FileChange } from "./git/walk.ts"; import { diffLines } from "./git/diff.ts"; import { topbar } from "./historical.tsx"; import { href, type Site, type View } from "./route.ts"; import { identityDate, looksBinary, looksGenerated, plural, utf8 } from "./util.ts"; const highlightedLines = (path: string, source: string) => import("./highlight.ts").then(module => module.highlightedLines(path, source)); export async function commitView(site: Site, view: View): Promise { const { repo } = site; const { oid } = view; await repo.prefetch([oid], { smart: "commit-diff" }); const commit = await repo.commit(oid); const parentTree = commit.parents.length > 0 ? (await repo.commit(commit.parents[0])).tree : null; const changes = await treeDiff(repo, parentTree, commit.tree); const blobs = changes.flatMap(change => [change.oldOid, change.newOid].filter(oid => oid !== null) ); await repo.prefetch(blobs, { depth: 0 }); const files = await Promise.all(changes.map(change => fileSection(site, oid, change))); const adds = files.reduce((n, f) => n + f.adds, 0); const dels = files.reduce((n, f) => n + f.dels, 0); const [subject, ...rest] = commit.message.trimEnd().split("\n"); const body = rest.join("\n").trim(); const meta = () as HTMLTableElement; const row = (key: string, value: Node | string) => meta.append( , ); row("author", `${commit.author.name} <${commit.author.email}>`); row("date", identityDate(commit.author)); row("commit", {oid}); for (const parent of commit.parents) { row("parent", {parent}); } if (commit.changeId) row("change-id", {commit.changeId}); const browse = browse files; return [ await topbar(site, view, [], [browse]),

{subject}

, ...(body ? [
{body}
] : []), meta,

+{String(adds)} -{String(dels)} {plural(changes.length, "changed file")}

, ...files.map(f => f.element), ]; } interface FileSection { element: HTMLElement; adds: number; dels: number; } async function fileSection(site: Site, revision: string, change: FileChange): Promise { const { repo } = site; const generated = looksGenerated(change.path); const actions = () as HTMLElement; if (change.status !== "modified") { actions.append({change.status}); } if (change.newOid) { const view = await href(site, { kind: "blob", oid: revision, path: change.path.split("/") }); actions.append(view); } if (generated) actions.append(generated); const header = ( {change.path} {actions} ) as HTMLElement; const section = (
{header}
) as HTMLDetailsElement; section.open = !generated; const [oldData, newData] = await Promise.all([ change.oldOid ? repo.blob(change.oldOid) : Promise.resolve(new Uint8Array()), change.newOid ? repo.blob(change.newOid) : Promise.resolve(new Uint8Array()), ]); if (looksBinary(oldData) || looksBinary(newData)) { section.append(

binary file

); return { element: section, adds: 0, dels: 0 }; } const oldText = utf8.decode(oldData); const newText = utf8.decode(newData); const hunks = diffLines(oldText, newText); const cells: Array<{ cell: HTMLTableCellElement; old: boolean; line: number }> = []; let adds = 0; let dels = 0; const table = (
{key} el.append(typeof value === "string" ? document.createTextNode(value) : value)} />
) as HTMLTableElement; for (const hunk of hunks) { table.append( , ); let aLine = hunk.aStart; let bLine = hunk.bStart; for (const line of hunk.lines) { const kind = line.sign === "+" ? "add" : line.sign === "-" ? "del" : "ctx"; const old = line.sign === "-"; const sourceLine = (old ? aLine : bLine) - 1; if (line.sign === "+") adds++; if (line.sign === "-") dels++; const cell = () as HTMLTableCellElement; cells.push({ cell, old, line: sourceLine }); table.append( {/* the +/- marker is CSS ::before so copied text stays clean */} {cell} , ); } } actions.prepend( +{String(adds)}, -{String(dels)}, ); section.append(table); void Promise.all([ change.oldOid ? highlightedLines(change.path, oldText) : Promise.resolve(null), change.newOid ? highlightedLines(change.path, newText) : Promise.resolve(null), ]).then(([oldLines, newLines]) => { for (const target of cells) { const line = (target.old ? oldLines : newLines)?.[target.line]; if (line) target.cell.replaceChildren(...line.map(node => node.cloneNode(true))); } }).catch(err => console.warn("arborium:", err)); return { element: section, adds, dels }; }
{`@@ -${hunk.aStart},${hunk.aLines} +${hunk.bStart},${hunk.bLines} @@`}
{line.text}
{line.sign === "+" ? "" : String(aLine++)} {line.sign === "-" ? "" : String(bLine++)}